Unity WebGL con compresión Brotli: HTTP headers por plataforma
Esta guía cubre el despliegue de builds de Unity WebGL que usan compresión Brotli (o Gzip): qué pedirle al programador de Unity antes del build, por qué son necesarios los headers HTTP, y cómo configurarlos según la plataforma de hosting.
¿Para qué sirve?
Unity WebGL puede compilar su build con tres modos de compresión. Cada modo genera archivos con extensiones distintas y, en dos de los tres casos, requiere que el servidor envíe un header Content-Encoding correcto para que el navegador pueda descomprimirlos.
| Modo | Extensión de archivos | Requiere headers del servidor |
|---|---|---|
| Brotli | .wasm.br, .framework.js.br, .data.br | Sí — Content-Encoding: br |
| Gzip | .wasm.gz, .framework.js.gz, .data.gz | Sí — Content-Encoding: gzip |
| Sin compresión | .wasm, .js, .data | No (solo Content-Type para .wasm) |
Cuando el servidor no envía el Content-Encoding correcto, el navegador recibe bytes comprimidos que trata como datos crudos. Los errores típicos son:
Unable to load file Build/MyGame.framework.js.br!
both async and sync fetching of the wasm failed
Estos errores no son del código de la app — son de headers HTTP faltantes o incorrectos.
Qué pedirle al programador de Unity antes del build
Antes de recibir el build, confirmar estos dos puntos con quien lo genera.
Tipo de compresión
Pedir que informe explícitamente si el build usa Brotli, Gzip o ninguna. Esto determina toda la configuración del servidor.
Ubicación en Unity:
Project Settings → Player → WebGL → Publishing Settings → Compression Format
Decompression Fallback (opción crítica)
En el mismo menú existe la opción Decompression Fallback. Si está activada, Unity incluye un descompresor JavaScript dentro del build: el navegador puede descomprimir los archivos sin necesidad de headers del servidor.
Pedir activada cuando:
- El hosting es Namecheap/cPanel con LiteSpeed (ver más abajo).
- No se puede controlar los headers HTTP de la plataforma.
- Entorno de desarrollo rápido sin configuración de servidor.
Tradeoff: Agrega ~0.5–2 segundos al tiempo de carga inicial. No afecta el rendimiento del juego en runtime.
No activar cuando:
- Se controla el servidor y se pueden configurar los headers correctos.
- El tamaño del build importa (Decompression Fallback lo agranda).
Regla práctica: Si no se conoce la plataforma de destino al momento del build, pedir con Decompression Fallback activado. Es el modo más robusto — elimina toda dependencia de configuración del servidor.
Headers HTTP requeridos (referencia universal)
Archivos .framework.js.br
Content-Encoding: br
Content-Type: application/javascript
Archivos .wasm.br
Content-Encoding: br
Content-Type: application/wasm
Archivos .data.br
Content-Encoding: br
Content-Type: application/octet-stream
Archivos .br (regla genérica para todos)
Content-Encoding: br
Cache-Control: max-age=31536000
Archivos .gz (regla genérica para todos)
Content-Encoding: gzip
Cache-Control: max-age=31536000
Archivos .wasm (sin compresión)
Content-Type: application/wasm
Por qué
Content-Type: application/wasmes obligatorio: Chrome y Firefox rechazan instanciar WebAssembly si el MIME type esapplication/octet-stream. El build falla silenciosamente o lanzawasm streaming compile failed.
Configuración por plataforma
Netlify
Método: Archivo netlify.toml en la raíz del repositorio.
[[headers]]
for = "/*.gz"
[headers.values]
Content-Encoding = "gzip"
Cache-Control = "max-age=31536000"
[[headers]]
for = "/*.br"
[headers.values]
Content-Encoding = "br"
Cache-Control = "max-age=31536000"
[[headers]]
for = "*.wasm"
[headers.values]
Content-Type = "application/wasm"
[[headers]]
for = "*.wasm.gz"
[headers.values]
Content-Type = "application/wasm"
Content-Encoding = "gzip"
[[headers]]
for = "*.wasm.br"
[headers.values]
Content-Type = "application/wasm"
Content-Encoding = "br"
Notas:
- Netlify aplica estas reglas sin conflictos ni re-compresión de archivos
.br/.gzexistentes. - Es la plataforma donde esta configuración funciona de forma más directa y predecible.
- No se necesita ninguna configuración adicional en el dashboard.
Render (Static Site)
Método: Dashboard → Static Site → pestaña Headers.
Diferencia crítica vs Netlify: El wildcard /*.br en Render solo matchea archivos en la raíz del sitio. Los builds de Unity viven en subcarpetas (ej. /unity/MiJuego/Build/MiJuego.wasm.br), por lo que el patrón correcto es /**/*.br con doble asterisco.
Configuración real (verificada):
| Request Path | Header Name | Header Value |
|---|---|---|
/*.br | Content-Encoding | br |
/*.br | Cache-Control | max-age=31536000 |
/*.gz | Content-Encoding | gzip |
/*.gz | Cache-Control | max-age=31536000 |
/*.wasm | Content-Type | application/wasm |
/*.wasm.br | Content-Type | application/wasm |
/*.wasm.br | Content-Encoding | br |
/*.wasm.gz | Content-Type | application/wasm |
/*.wasm.gz | Content-Encoding | gzip |
/*.framework.js.br | Content-Type | application/javascript |
/*.framework.js.br | Content-Encoding | br |
/*.data.br | Content-Type | application/octet-stream |
/*.data.br | Content-Encoding | br |
Problema conocido — doble compresión: Render puede tener compresión Brotli autom ática. Si ya sirve un archivo .br pre-comprimido, puede intentar comprimirlo de nuevo. Síntoma: el build falla incluso con los headers correctos (ver diagnóstico más abajo).
Problema conocido — CDN por delante (F5/ves.io, Cloudflare): Si hay un CDN frente a Render, puede cachear respuestas antiguas (sin los headers nuevos) aunque ya se haya redesplegado. Solución: purgar caché del CDN después de cada deploy, o testear directo sobre la URL .onrender.com para aislar si el problema es el CDN o Render.
BunnyCDN
BunnyCDN tiene dos componentes que se usan juntos para servir Unity WebGL.
Paso 1 — Crear un Storage Zone. El Storage Zone es donde se suben físicamente los archivos del build.
- BunnyCDN Dashboard → Storage → Add Storage Zone
- Nombre: ej.
bhuegol - Tier: Standard
- Replication: activado (recomendado)
- Subir los archivos del build de Unity directamente al Storage Zone
El Storage Zone actúa como origin. No tiene URL pública directa para servir al browser — para eso se necesita el Pull Zone.
Paso 2 — Crear un Pull Zone conectado al Storage Zone. El Pull Zone es la capa CDN que sirve los archivos al browser con caché global.
- BunnyCDN Dashboard → CDN → Add Pull Zone
- Nombre: ej.
bhuegol - Origin: seleccionar el Storage Zone
bhuegol(no una URL externa) - Esto genera una URL pública como
https://bhuegol.b-cdn.net
Paso 3 — Configurar Edge Rules en el Pull Zone. Los headers no se configuran en el Storage Zone sino en el Pull Zone → Edge Rules.
Edge Rule 1 — .data.br
Condition: Request URL matches wildcard → *.data.br*
Actions:
- Set Response Header → Content-Encoding: br
- Set Response Header → Content-Type: application/octet-stream
Edge Rule 2 — .wasm.br
Condition: Request URL matches wildcard → *.wasm.br*
Actions:
- Set Response Header → Content-Encoding: br
- Set Response Header → Content-Type: application/wasm
Edge Rule 3 — .framework.js.br
Condition: Request URL matches wildcard → *.framework.js.br*
Actions:
- Set Response Header → Content-Encoding: br
- Set Response Header → Content-Type: application/javascript
El orden importa: colocar las reglas más específicas primero (
.wasm.brantes que.brgenérico). Si se agregan reglas para.gz, seguir el mismo patrón.
Importante — desactivar re-compresión: En el Pull Zone → Optimizer, desactivar cualquier opción de compresión automática sobre los paths del build. BunnyCDN puede intentar re-comprimir archivos .br ya comprimidos y corromper el contenido.
Ventaja clave de BunnyCDN: El CDN está downstream del storage y puede inyectar headers independientemente del origin. Esto permite servir Unity correctamente desde Azure Blob Storage, Namecheap, o cualquier origin que no soporte configuración de headers. Cache HIT rate esperado: ~99%+ una vez calentado.
Azure Blob Storage
Azure Blob Storage en modo Static Website no permite configurar Content-Encoding por archivo desde el portal. Servir un build Brotli de Unity directamente desde Azure sin CDN no es viable salvo que el build tenga Decompression Fallback.
Solución estándar: Usar Azure Blob como storage y colocar BunnyCDN como Pull Zone apuntando al Blob como origin, luego configurar las Edge Rules en BunnyCDN.
Alternativa sin CDN: Solicitar el build con Decompression Fallback activado.
Namecheap / cPanel — caso problemático (LiteSpeed)
Namecheap shared hosting corre LiteSpeed en lugar de Apache puro. Esto genera un problema específico e irresolvible con .htaccess.
El problema
LiteSpeed lee el .htaccess de forma compatible con Apache (por eso AddType/Content-Type funciona), pero maneja la compresión internamente y descarta el header Content-Encoding que se intente definir vía .htaccess.
Síntomas:
Content-Typese aplica correctamente ✅Content-Encoding: brno aparece en los response headers ❌- Los headers muestran
server: LiteSpeedyx-turbo-charged-by: LiteSpeed
Cómo confirmar:
curl -I https://tu-subdominio.com/Build/MiJuego.wasm.br | grep -i "server\|x-turbo\|content-encoding"
Qué no funciona en LiteSpeed (no perder tiempo en esto)
Ninguna de estas directivas logra poner Content-Encoding: br:
AddEncoding br .br # ignorado
Header set Content-Encoding "br" # ignorado
Header always set Content-Encoding "br" # ignorado
LiteSpeed administra compresión internamente y no lo delega al .htaccess en shared hosting (requeriría acceso WHM/root que no existe en planes compartidos).
Soluciones
Opción A — Decompression Fallback (recomendada): Pedir al programador Unity que regenere el build con Decompression Fallback activado. No requiere ningún header del servidor.
Opción B — BunnyCDN por delante del subdominio: Crear un Pull Zone en BunnyCDN con el subdominio de Namecheap como origin URL, y configurar las Edge Rules ahí. El CDN actúa como intermediario e inyecta los headers que LiteSpeed no puede proveer.
Diagnóstico rápido (checklist)
Cuando un build de Unity WebGL falla al cargar, seguir este orden:
Paso 1 — Identificar el error en consola
Unable to load file Build/...→Content-Encodingfaltante o incorrecto.both async and sync fetching of the wasm failed→Content-Type: application/wasmfaltante oContent-Encodingincorrecto.Failed to fetchcon status 404 → el archivo no existe en el path.
Paso 2 — Revisar headers de respuesta
En DevTools → Network → seleccionar el archivo .wasm.br → Response Headers. Deben aparecer:
content-encoding: br
content-type: application/wasm
O con curl:
curl -I -H "Accept-Encoding: br" https://tu-dominio.com/Build/MiJuego.wasm.br
Paso 3 — Identificar el servidor
server: LiteSpeed+x-turbo-charged-by: LiteSpeed→ Namecheap,.htaccessno sirve paraContent-Encoding.server: cloudflare→ configurar Page Rules o Transform Rules en Cloudflare.- Sin
serverheader → probablemente Render o similar, configurar en dashboard.
Paso 4 — Aislar capas. Si hay CDN por delante, testear directamente la URL del origin (ej. .onrender.com, bhuegol.b-cdn.net) para saber si el problema viene del CDN o del origin.
Paso 5 — Verificar caché. Probar siempre en ventana de incógnito. Un hit de caché puede mostrar headers viejos aunque ya se haya redesplegado. En BunnyCDN usar el botón Purge Cache del Pull Zone después de cambiar Edge Rules.
Resumen de decisión por plataforma
¿En qué plataforma se hace el deploy?
│
├── Netlify
│ └── netlify.toml con [[headers]] en la raíz ✅
│ Más simple y predecible de todas las opciones.
│
├── Render (Static Site)
│ └── Dashboard → Headers, usar /*.br (no /*.br) ✅
│ Si hay CDN por delante: purgar caché del CDN tras deploy.
│
├── BunnyCDN como CDN principal
│ ├── Paso 1: Crear Storage Zone y subir los archivos del build
│ ├── Paso 2: Crear Pull Zone apuntando al Storage Zone
│ └── Paso 3: Edge Rules por extensión (.data.br, .wasm.br, .framework.js.br) ✅
│ Desactivar Optimizer/auto-compresión en el Pull Zone.
│
├── Azure Blob Storage
│ └── No soporta Content-Encoding nativo → necesita BunnyCDN por delante ✅
│ O usar Decompression Fallback en el build.
│
└── Namecheap / cPanel (LiteSpeed)
├── NO usar .htaccess para Content-Encoding — LiteSpeed lo ignora ❌
├── Opción A: Decompression Fallback en el build ✅ (recomendada)
└── Opción B: BunnyCDN Pull Zone apuntando al subdominio como origin ✅